Move queue state out of analysis JSON into real columns#11
Merged
Conversation
… appids 007: add status/processing_started/failure_reason/failure_retryable columns to apps, backfilled from the analysis JSON state (NULL->queued, 'Processing in progress'->processing, success=false->failed, else analysed). Malformed processing timestamps fall back to NOW() instead of aborting the migration. Partial indexes support the queue scan. 008: modernise types (json->jsonb, timestamp->timestamptz assuming UTC). The pg driver treats these equivalently, so the currently deployed code keeps working during the deploy window. 009: enforce case-insensitive bundle IDs by merging rows that differ only by case (dedupe colliding app_analyses history before repointing to the keeper) and adding a unique index on lower(appid). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the analysis-JSON state machine with the real status columns: - findApp: case-insensitive lookup (lower(appid) = lower($1)). - countQueue / countAnalysed / getAllApps / getSiteDataSignature / lastAnalysed: filter on status instead of analysis JSON. - nextApp: pick candidates by status (queued, expired processing lock, stale analysed, or retryable failed) and take a non-destructive lock (status='processing', processing_started=NOW()) without overwriting the analysis payload. History snapshots now happen only in updateAnalysis. - updateAnalysis: still writes the raw payload to apps.analysis (website failure display + Pi HTTP responses stay byte-identical) and additionally sets status/failure_reason/failure_retryable via the new pure deriveAnalysisState helper, which is unit-tested. The Raspberry Pi analyser HTTP contract (/queue, /uploadAnalysis, /reportAnalysisFailure, /ping) is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
queue-refetch, priority-report and queue-status now classify queue/lock/ failure state via the status/processing_started/failure_retryable columns instead of analysis-JSON predicates. queue-refetch also resets status back to 'queued' (and clears the failure/lock columns) when it queues a refetch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ict handling The status-column port made status='failed' AND failure_retryable rows immediately eligible in nextApp, but the old JSON predicate only retried failures once their analysisversion was outdated or the analysis was stale — without that gate a popular, persistently failing app would be handed to the analyser in a tight loop and starve the queue. Restore the gate in nextApp and the two reporting scripts, and shape the failed partial index to match. addApp now uses a bare ON CONFLICT so inserts racing across case-variants of the same bundle ID (unique lower(appid) from 009) are ignored instead of erroring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kasnder
force-pushed
the
claude/db-state-machine
branch
from
July 13, 2026 13:37
7e72747 to
df880c2
Compare
Member
Author
|
Independent review of the subagent's work — three issues found and fixed in df880c2:
Verified clean: 007 backfill CASE order (processing-before-failed), timestamp regex fallback, 008 UTC reinterpretation, 009's dedupe-before-repoint around the Remaining known risk (unchanged from the PR description): migrations reviewed but not executed against a real Postgres — recommend eyeballing the Railway deploy logs when this merges. |
kasnder
marked this pull request as ready for review
July 13, 2026 13:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
apps.analysisJSON column previously encoded four different things at once:NULL= queued{success:false, logs:'Processing in progress', timestamp}= worker lock{success:false, reason, retryable}= failure recordThis PR moves queue/lock/failure state into real columns and drives all scheduling off them, while keeping
apps.analysisas the untouched raw analyser payload.State machine
New columns on
apps(migration 007):status text NOT NULL DEFAULT 'queued'CHECK IN (queued,processing,analysed,failed)processing_started timestamptz— when the current processing lock was takenfailure_reason text,failure_retryable boolean— populated only forfailedTransitions:
addAppinserts a row →queued(column default).nextApppicks a candidate (queued, expiredprocessinglock, staleanalysed, or retryablefailed) withFOR UPDATE SKIP LOCKED, then setsstatus='processing',processing_started=NOW(). This lock is non-destructive — it no longer overwritesanalysis, so the last good result stays visible on the website during a refetch.updateAnalysiswrites the new payload and setsstatus='analysed'orstatus='failed'(+failure_reason/failure_retryable) via the pure, unit-testedderiveAnalysisStatehelper. It still writes the raw payload toapps.analysisand still snapshots history intoapp_analyses(ON CONFLICT DO NOTHING).Migrations & safety
Migrations run on deploy before the new code boots, and must tolerate the currently deployed code running briefly against the migrated schema. All three are additive/backward-compatible:
success=false, so it is classified first). Malformed processing timestamps are regex-validated and fall back toNOW()so a bad value can't abort the migration. Partial indexes back the queue scan. Old code ignores these columns.json→jsonbandtimestamp→timestamptz(values assumed UTC viaAT TIME ZONE 'UTC'). Thepgnode driver treats json/jsonb and timestamp/timestamptz equivalently on both read and write, so the still-running old code keeps working.app_analysesrows around the(appid, analysed)unique index before repointing history to the keeper), then a unique index onlower(appid). Noappidvalues are rewritten, so the existing validatedapps_appid_validCHECK still holds.Migration ordering 007 → 008 → 009 is enforced by the runner's sorted-filename ledger.
During the brief old-code window a row may end up with
status='queued'but an oldProcessing in progressJSON marker (or vice-versa); this self-heals on the nextnextAppand is harmless (idempotent,SKIP LOCKED).API freeze — Raspberry Pi analyser requires NO changes
The analyser talks to this server only over HTTP. All endpoints it uses are byte-identical in shape:
/queuestill returnsapp.appid(nextAppreturns{appid})./uploadAnalysisand/reportAnalysisFailurestill flow throughupdateAnalysis, which returns the samepgresult (RETURNINGappid, details, analysed) and still writes the failure JSON toapps.analysisso the website's failure display keeps working./pingunchanged.Scripts
queue-refetch,priority-report, andqueue-statusnow classify state via the status columns.queue-refetchalso resetsstatus='queued'and clears the lock/failure columns when queueing a refetch.Tests
npm test(node --test): 22/22 pass, including newderiveAnalysisStatecoverage. No local Postgres was available, so the migration SQL was validated by careful review rather than execution.🤖 Generated with Claude Code